李守中
该站已迁往根域名 https://lishouzhong.com
需要注意,迁移后的文章的 url 可能会发生变化。
域名 https://note.lishouzhong.com 下的内容将不再更新,但已有内容会永久保留。

C 语言中 inline 关键字的行为

Table of Contents

inline 关键字是给编译器的 建议 。当 inline 关键字起作用时,被标注了 inline 的内联函数可以嵌入调用方的函数体中,以省略一次函数调用的开销。

inline 关键字在 C99 标准中首次出现,但在这之前,已经有编译器支持这个关键字了 (比如 GCC),只是不同编译器对于这个关键字的处理方式不太一样。不多展开。之后的内容用 GCC 来作讲解。

static inline 关键字组合的行为过于简单,略过。后文内容基于 C99 标准。

1. 只有启用编译优化时 inline 才生效

现在有 main.c 和 fun.h 两个文件:

// main.c
#include <stdio.h>

int main(){
    f1();
    return 0;
}

// fun.h
#include <stdio.h>

#ifndef __FUN_
#define __FUN_

inline void f1() {printf("inline f1 in %s\n", __FILE__);}

#endif

注: 不用纠结 f1 定义在 main.c 里还是 fun.h 里。在编译 main.c 的时候,main.c 会被展开成这样:

// main.c
#include <stdio.h>

inline void f1(){
    printf("inline f1 in %s\n", __FILE__);
}

int main(){
    f1();
    return 0;
}

现在使用 C99 标准,不开编译优化,编译得到:

$ gcc -O0 -std=gnu99 main.c
/usr/bin/ld: /tmp/ccPM3vFm.o: in function `main':
main.c:(.text+0xa): undefined reference to `f1'
collect2: error: ld returned 1 exit status

结果是,f1 这个函数并没有被按外部函数编译,导致链接器没有找到 f1 这个符号。但可以确定的是,C99 标准支持这样定义内联函数。

注: 外部函数,是函数定义中用了 extern 关键字的函数。如果函数定义不加 {inline | static | inline static} 关键字,则这个函数默认使用了 extern 关键字。

原因在于,inline 关键字只有在开启编译优化选项时才会生效。现在使用编译优化得到:

$ gcc -O2 -std=gnu99 main.c
$ ./a.out
inline f1 in main.c

在 -O2 这个编译优化级别下,main 函数中的 f1 使用了 inline 定义的内容。通过汇编代码也能看出,main.s 中并没有显式地使用 call 来调用 f1,函数调用被优化掉了:

$ gcc -O2 -std=gnu99 main.c -S
$ cat main.s
        .file   "main.c"
        .text
        .section        .rodata.str1.1,"aMS",@progbits,1
.LC0:
        .string "main.c"
.LC1:
        .string "inline f1 in %s\n"
        .section        .text.startup,"ax",@progbits
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB12:
        .cfi_startproc
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $.LC0, %esi
        movl    $.LC1, %edi
        xorl    %eax, %eax
        call    printf
        xorl    %eax, %eax
        addq    $8, %rsp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE12:
        .size   main, .-main
        .ident  "GCC: (GNU) 7.3.0"
        .section        .note.GNU-stack,"",@progbits

2. 一个函数可以有内联函数和外部函数两个定义

此时,在另一个文件 fun.c 中提供一个 f1 函数的外部函数定义:

// fun.c
#include <stdio.h>

inline void f1(){
    printf("inline f1 in %s\n", __FILE__);
}

然后把 main.c 和 fun.c 放在一起编译,不开编译优化,得到:

$ gcc -O0 -std=gnu99 main.c fun.c
$ ./a.out
inline f1 in fun.c

从输出可以看到,链接器为 main.c 中的 f1 选择了 fun.c 中的定义。这意味着,一个函数可以有内联函数和外部函数两个定义,这两个定义不冲突。

但如果打开编译优化,得到:

$ gcc -O2 -std=gnu99 main.c fun.c
$ ./a.out
inline f1 in main.c

可以看到,打开编译优化以后,main.c 中的 f1 函数优先使用了内联函数定义。

原因在于,main.c 和 fun.c 处于两个不同的编译单元:

  • 在不开启编译优化 (不使用内联优化) 的情况下,main.c 编译出的 main.o 中的 f1 只是一个未定义的符号:
    • 如果 f1 没有在其他编译单元中定义 (比如 fun.c 中),那么链接器就会报一个找不到引用的 bug
    • 如果 f1 在 fun.c 中有外部定义,那么 fun.c 编译出的 fun.o 中就会有一个 global 范围的 f1 函数。这个 f1 会被链接器发现并使用
  • 在开启编译优化 (使用内联优化) 的情况下,编译器在编译 main.o 这个编译单元时直接对 f1 做了优化:
    • 使用 f1 的 inline 定义并内联到 main 函数中
    • main.o 中不为 f1 生成 global 范围的 f1 函数
    • fun.o 中的 f1 没有被 main.o 使用

3. 如何让编译优化参数不影响执行结果

正经写代码时,函数在有 inline 定义和没有 inline 定义时的行为必须一致。必须避免使用 gcc -O2 能过编译,而使用 gcc -O0 过不了编译的情况。

现在修改 fun.h 中的内容为:

// fun.h
#ifndef __FUN_
#define __FUN_

extern void f1();

#endif

并将 f1 函数的实现写在 main.c 中:

// main.c
#include <stdio.h>
#include "fun.h"

inline void f1(){
    printf("inline f1 in %s\n", __FILE__);
}

int main(){
    f1();
    return 0;
}

注: main.c 中的 f1 在编译时按 extern inline void f1() {printf("inline f1 in %s\n", __FILE__);} 处理。

分别在关闭编译优化和开启编译优化后,编译并执行:

$ gcc -O0 -std=gnu99 main.c
$ ./a.out
inline f1 in main.c

$ gcc -O2 -std=gnu99 main.c
$ ./a.out
inline f1 in main.c

可以看到,不论编译优化是否开启,程序编译执行后的结果都一样。

来看编译优化是如何影响编译结果。

不用编译优化的情况下,得到汇编代码:

$ gcc -O0 -std=gnu99 main.c -S
$ cat main.s
        .file   "main.c"
        .text
        .section        .rodata
.LC0:
        .string "main.c"
.LC1:
        .string "inline f1 in %s\n"
        .text
        .globl  f1
        .type   f1, @function
f1:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movl    $.LC0, %esi
        movl    $.LC1, %edi
        movl    $0, %eax
        call    printf
        nop
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   f1, .-f1
        .globl  main
        .type   main, @function
main:
.LFB1:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movl    $0, %eax
        call    f1
        movl    $0, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE1:
        .size   main, .-main
        .ident  "GCC: (GNU) 7.3.0"
        .section        .note.GNU-stack,"",@progbits

可以看到,内联优化并未生效,main 函数调用 f1 使用的是 call 指令。但在 fun.h 中的 extern void f1(); 声明让编译器为 f1 函数生成了一份独立的汇编代码,这份汇编代码可以直接被外部函数调用。也就是说,在未开启编译优化的情况下,f1 按外部函数来处理。

开启编译优化的情况下,得到汇编代码:

$ gcc -O2 -std=gnu99 main.c -S
$ cat main.s
        .file   "main.c"
        .text
        .section        .rodata.str1.1,"aMS",@progbits,1
.LC0:
        .string "main.c"
.LC1:
        .string "inline f1 in %s\n"
        .text
        .p2align 4,,15
        .globl  f1
        .type   f1, @function
f1:
.LFB11:
        .cfi_startproc
        movl    $.LC0, %esi
        movl    $.LC1, %edi
        xorl    %eax, %eax
        jmp     printf
        .cfi_endproc
.LFE11:
        .size   f1, .-f1
        .section        .text.startup,"ax",@progbits
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB12:
        .cfi_startproc
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $.LC0, %esi
        movl    $.LC1, %edi
        xorl    %eax, %eax
        call    printf
        xorl    %eax, %eax
        addq    $8, %rsp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE12:
        .size   main, .-main
        .ident  "GCC: (GNU) 7.3.0"
        .section        .note.GNU-stack,"",@progbits

可以看到内联优化生效了,main 函数中并未使用 call 指令调用 f1 函数,并且编译器仍然为 f1 生成了一份独立的汇编代码,这份汇编代码可以直接被外部函数调用。也就是说,在开启编译优化的情况下,f1 函数在本编译单元内按内联函数处理,在其他编译单元调用 f1 函数时按外部函数处理。

4. 多个编译单元共享一份内联函数

虽然这个方法,使得 f1 函数既可以作为内联函数,也可以作为外部函数被使用,但当它作为内联函数时,仅在定义这个内联函数的编译单元 (这里是 main.c) 内有效。

为了让多个编译单元共享一份内联函数,并且这个内联函数同时也可以作为外部函数使用,就 必须把内联函数的定义放在头文件里

// fun.h
#ifndef __FUN_
#define __FUN_

inline void f1(){
    printf("inline f1 in %s\n", __FILE__);
}

#endif
// main.c
#include <stdio.h>
#include "fun.h"

int main(){
    f1();
    return 0;
}

但是要注意,不要把内联函数定义到其他 .c 文件中。这会导致编译器无法将 main .c 中对于 f1 的调用转换为内联函数调用。 很多人喜欢仅在 fun.h 中声明一个外部函数 f1 (默认加 extern 关键字),再把 f1 的内联实现写在 fun.c 文件中:

// fun.h
#ifndef __FUN_
#define __FUN_

void f1();

#endif



// fun.c
#include <stdio.h>
#include "fun.h"

inline void f1(){
    printf("inline f1 in %s\n", __FILE__);
}



// main.c
#include <stdio.h>
#include "fun.h"

int main(){
    f1();
    return 0;
}

注: fun.c 中的 f1 在编译时按 extern inline void f1() {printf("inline f1 in %s\n", __FILE__);} 处理。

这个写法,不论开不开编译优化,最终得到的程序都会把 main.c 中对 f1 的调用处理成外部函数调用。 因为 main.c 这个编译单元里压根没有 f1 的内联函数定义,但链接器在 fun.o 里能找到 f1 的外部函数定义。

从前面的案例可以知道,当函数 f1 在声明时有 extern 关键字,在定义时有 inline 关键字的情况下 (效果等同于在定义函数时使用 extern inline 关键字组合),编译器一定会为 f1 生成一段独立的汇编代码,所以在 main.c 中调用 f1 时,可以以调用外部函数的方式调用 fun.c 中的 f1 函数。

而在 fun.c 这个编译单元中,如果 fun.c 中有对于 f1 的调用,那么 fun.c 中对于 f1 的调用会被处理成内联函数调用。

5. 行为总结

  • 在 fun.h 内定义 inline void f1() {printf("inline f1 in %s\n", __FILE__);}
  • 在 main.c 内 include "fun.h"
  • 在 main.c 的 main 函数内调用 f1
  • 编译:
    • gcc -O0 -std=gnu99 main.c 结果 f1 未定义
    • gcc -O2 -std=gnu99 main.c 结果 f1 按内联函数处理
  • 在 fun.h 内声明 extern void f1();
  • 在 main.c 内 include "fun.h" ,定义 inline void f1() {printf("inline f1 in %s\n", __FILE__);}
  • 在 main.c 的 main 函数内调用 f1
  • 编译:
    • gcc -O0 -std=gnu99 main.c fun.c 结果 f1 按外部函数处理 (f1 有独立的汇编代码)
    • gcc -O2 -std=gnu99 main.c fun.c 结果 f1 按内联函数处理 (f1 有独立的汇编代码)
  • 在 fun.h 内声明 extern void f1();
  • 在 fun.c 内定义 inline void f1() {printf("inline f1 in %s\n", __FILE__);}
  • 在 main.c 内 include fun.h 并调用 f1
  • 不论开不开编译优化,f1 都按外部函数处理 (因为 f1 都定义在另一个编译单元 fun.c 里了,所以肯定要按外部函数处理)

需要注意的是,C99 标准中并没有显式规定编译器如何处理使用 extern inline 关键字组合定义的函数。下面这个列表中的结论是观察编译结果得出的。

  • 在 fun.h 内定义 extern inline void f1() {printf("inline f1 in %s\n", __FILE__);}
  • 在 main.c 内 include "fun.h"
  • 在 main.c 的 main 函数内调用 f1
  • 编译:
    • gcc -O0 -std=gnu99 main.c 结果 f1 按外部函数处理 (f1 有独立的汇编代码)
    • gcc -O2 -std=gnu99 main.c 结果 f1 按内联函数处理 (f1 有独立的汇编代码)

可以看出,组合 2 和组合 4 的编译行为一致。

6. GNU C89 中的 inline

尽管 GCC 早在 GNU C89 标准中就将 inline 作为 c extension 支持了,但是 GNU C89 标准发布时,inline 关键字还没有进入 C 语言标准中。

这就导致 C89 和 C99 两个标准中对于 inline 关键字行为的定义并不相同。具体表现为:

  • C89 中的 inline 对应 C99 中的 extern inline
  • C89 中的 extern inline 对应 C99 中的 inline

即,C89 中 inline 和 extern inline 的行为与 C99 相反。



Last Update: 2023-05-18 Thu 09:10

Generated by: Emacs 28.2 (Org mode 9.5.5)   Contact: lsz.sino@outlook.com

若正文中无特殊说明,本站内容遵循: 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议